<!--
 * @FileDescription: 封装了 el-table 的基础表格组件，主要调整了基本的样式，以及分页器实现，具体的列由外部注入
 -->
<script setup lang="ts">
import SelectText from '@/components/select/SelectText.vue';
import { ReloadOutlined } from '@ant-design/icons-vue';
import _ from 'lodash';

defineOptions({
  inheritAttrs: false, // 不自动继承属性，因为 el-table 不是根节点
});

export interface TableBaseProps {
  data: any[]; // 表数据
  // 如果开启后端分页，下面两个值至少需要传入一个; 如果同时传入，优先使用 pageCount
  total?: number; // 总条数，前端分页模式传入无效
  pageCount?: number; // 总页数，前端分页模式传入无效
  emptyText?: string; // 表数据为空时显示的文本内容， 也可以通过 #empty 设置
  errorMsg?: any; // 错误信息，如果传入，将在表数据为空时代替 emptyText 显示
  retryFn?: () => void; // 重试函数，如果传入，将在 errorText 不为空且表数据为空时显示重试按钮
  showFooter?: boolean; // 是否显示底栏 (选择条数文字提示和分页器)
  withSelection?: boolean; // 在第一列显示复选框
  singleSelect?: boolean; // 是否只允许单选
  withIndex?: boolean; // 在第一列显示序号 (如果有复选框，序号在复选框后)，序号会自动根据当前页码和每页条数计算
  indexWidth?: string | number; // 序号列的宽度
  showPagination?: boolean; // 是否显示分页器
  pageSizes?: number[]; // 分页器的每页条数选项
  remotePagin?: boolean; // 是否开启后端分页模式
  reserveSelection?: boolean; // 在后端分页模式中，跨页面保留选中状态; 注意: 如果一条数据被选中后，在翻页后被后端删除，导致这条数据无法再显示回表中，那么用户将无法取消它的选中状态
  rowKey?: string; // 行数据的唯一标识字段; 必须传入，默认为 'id'
  filter?: (v: any, i: number, arr: any[]) => boolean; // 自定义过滤函数，用于前端搜索等场景
}

const props = withDefaults(defineProps<TableBaseProps>(), {
  total: 0,
  pageCount: undefined,
  emptyText: '暂无数据',
  errorMsg: undefined,
  retryFn: undefined,
  showFooter: true,
  withSelection: false,
  singleSelect: false,
  withIndex: false,
  indexWidth: 65,
  showPagination: true,
  pageSizes: () => [5, 10, 15, 20, 30, 40, 50, 100],
  remotePagin: false,
  reserveSelection: false,
  rowKey: 'id',
});

const selectedItems = defineModel<any[]>('selectedItems', { default: [] }); // 选中的条目
const currentPage = defineModel<number>('page', { default: 1 }); // 当前页码，供分页器使用
const pageSize = defineModel<number>('pageSize', { default: 10 }); // 每页条数，供分页器使用

// 通过一个 ref 中转 selectedItems，因为在 ModelRef 上使用 ignoreUpdates 不起作用
const selected = ref<any[]>([]);
syncRef(selectedItems, selected);

// ! 在内部修改 selected 时，必须使用 updateSelected
const { ignoreUpdates: updateSelected } = watchIgnorable(selected, (v, old) => {
  // 用户在外部修改了 selectedItems，修改表格中的选中状态
  if (props.singleSelect) {
    // 单选模式，简单处理即可
    radio.value = v.length ? v[0][props.rowKey] : undefined;
    return;
  }

  if (!tableRef.value) return;
  const removed = _.differenceWith(old, v, (a, b) => a[props.rowKey] === b[props.rowKey]);
  const added = _.differenceWith(v, old, (a, b) => a[props.rowKey] === b[props.rowKey]);
  tableRef.value.toggleRowSelection(removed, false);
  tableRef.value.toggleRowSelection(added, true);
});

const emit = defineEmits(['selection-change']); // 将内部有处理的事件向上传递

const tableRef = ref(); // 表格实例
defineExpose({ tableRef }); // 供外部使用表格实例方法

const ignoreSelectionChangeFlag = ref(false); // 跨页保留选中: 在页面刷新过程中需要忽略选中状态的改变
function onSelectionChange(val: any[]) {
  if (!props.remotePagin || !props.reserveSelection) {
    // 非后端分页模式，或未开启 reserveSelection
    updateSelected(() => {
      selected.value = val;
    });
    emit('selection-change', val);
  } else {
    // 页面正在刷新过程中，不处理选中状态的改变
    if (ignoreSelectionChangeFlag.value) return;
    // 计算出已选中但不在当前页的数据
    const diff = _.differenceWith(
      selected.value,
      props.data,
      (a, b) => a[props.rowKey] === b[props.rowKey]
    );
    // 将当前页更新的数据与之前的合并
    updateSelected(() => {
      selected.value = diff.concat(val);
    });

    emit('selection-change', selected.value);
  }
}
watchDeep(
  () => props.data,
  () => {
    if (!props.remotePagin || !props.reserveSelection) return;
    // 跨页保留选中: 页面开始刷新时，开始忽略选中状态的改变
    ignoreSelectionChangeFlag.value = true;
  },
  { flush: 'pre' } // 在页面渲染前执行
);
watchDeep(
  () => props.data,
  () => {
    if (!props.remotePagin || !props.reserveSelection) return;
    // 跨页保留选中: 表数据更新后，将存在 selected 中的行切换为选中状态
    props.data.forEach((v) => {
      const row = selected.value.find((s) => s[props.rowKey] === v[props.rowKey]);
      if (row) {
        tableRef.value?.toggleRowSelection(v, true);
      }
    });
    // 页面刷新完成，停止忽略选中状态的改变
    ignoreSelectionChangeFlag.value = false;
  },
  { flush: 'post' } // 需要在页面渲染完成后执行
);

// 过滤后的数据; 前端分页时用它的长度来计算分页
const filteredData = computed(() => {
  let d = props.data;
  // 用户自定义过滤器
  if (props.filter) {
    d = d.filter(props.filter);
  }
  return d;
});
// 表格中实际显示的数据
const tableData = computed(() => {
  let d = filteredData.value;
  if (!props.remotePagin && props.showPagination) {
    // 前端分页
    d = d.filter(
      (_, i) =>
        i >= (currentPage.value - 1) * pageSize.value && i < currentPage.value * pageSize.value
    );
  }
  return d;
});

function indexMethod(index: number) {
  // 计算序号
  return (currentPage.value - 1) * pageSize.value + index + 1;
}

const jumpTo = ref(); // "跳转到"输入框
const onJumpToTrigger = () => {
  // 跳转到指定页
  if (!jumpTo.value) return;
  const to = parseInt(jumpTo.value);
  jumpTo.value = '';
  if (isNaN(to) || to < 1) return;

  // 确认最大页数
  let maxPage = 1;
  if (props.remotePagin) {
    // 后端分页，优先检查 pageCount
    if (props.pageCount) {
      maxPage = props.pageCount;
    } else {
      maxPage = Math.max(Math.ceil(props.total / pageSize.value), 1);
    }
  } else {
    // 前端分页
    maxPage = Math.ceil(filteredData.value.length / pageSize.value);
  }

  if (to > maxPage) {
    currentPage.value = maxPage;
    return;
  }
  currentPage.value = to;
};

const radio = ref(); // 单选框
watch(radio, (v) => {
  // 单选框选中时，触发事件
  if (v == null) {
    updateSelected(() => {
      selected.value = [];
    });
    return;
  }
  const item = props.data.find((d) => d[props.rowKey] === v);
  updateSelected(() => {
    selected.value = item ? [item] : [];
  });
  emit('selection-change', selected.value);
});

const rowStyle = () => {
  return {
    height: '55px',
  };
};
</script>

<template>
  <div class="flex flex-col gap-5 rounded-lg bg-white p-4">
    <!-- 表 -->
    <el-table
      ref="tableRef"
      :data="tableData"
      class="w-full border border-gray-300"
      :header-cell-class-name="'header-cell'"
      :header-cell-style="{
        textAlign: 'center',
        background: 'rgb(41, 156, 12)',
        fontSize: '18px',
        color: 'white',
      }"
      :cell-style="{
        textAlign: 'center',
        fontSize: '12px',
        color: 'black',
      }"
      :empty-text="emptyText"
      @selection-change="onSelectionChange"
      :row-style="rowStyle"
      v-bind="$attrs"
    >
      <template v-if="withSelection">
        <!-- 多选框 -->
        <el-table-column v-if="!singleSelect" type="selection" width="32" />
        <!-- 单选框 -->
        <el-table-column v-else width="32">
          <template #default="{ row }">
            <el-radio v-model="radio" :value="row[rowKey]" />
          </template>
        </el-table-column>
      </template>

      <el-table-column v-if="withIndex" type="index" :width="indexWidth" :index="indexMethod" />
      <slot></slot>

      <template #append>
        <slot name="append"></slot>
      </template>

      <template #empty>
        <slot name="empty">
          <div v-if="errorMsg" class="flex flex-col leading-normal">
            <div v-if="retryFn" class="mb-2">
              <el-button type="primary" link @click="retryFn">
                <ReloadOutlined :style="{ fontSize: '20px' }" class="mr-2" />
                数据加载失败，点击重试
              </el-button>
            </div>

            <div>{{ errorMsg }}</div>
          </div>
        </slot>
      </template>
    </el-table>

    <!-- 底部 -->
    <div
      v-if="showFooter && (withSelection || showPagination)"
      class="mx-1 flex items-center justify-between gap-2 tracking-tighter"
    >
      <div v-if="withSelection" class="text-[14px] font-[500] text-[#666666]">
        当前选中
        <span class="text-primary">{{ selected.length }}</span>
        条
      </div>

      <!-- 分页器 -->
      <div
        v-if="showPagination"
        class="flex items-center gap-2 text-[14px] font-[500] text-[#666666]"
      >
        <div class="flex items-center gap-1">
          <span class="mr-1">每页</span>
          <div class="w-10">
            <SelectText v-model="pageSize">
              <el-option v-for="s in pageSizes" :key="s" :value="s" />
            </SelectText>
          </div>
          <span>条</span>
        </div>

        <el-pagination
          background
          layout="prev, pager, next"
          :page-size="pageSize"
          v-model:current-page="currentPage"
          :total="remotePagin ? total : filteredData.length"
          :page-count="remotePagin ? pageCount : undefined"
        />

        <div class="flex items-center gap-2">
          <span>跳至</span>
          <div class="jump_to w-12">
            <el-input v-model="jumpTo" @blur="onJumpToTrigger" @keyup.enter="onJumpToTrigger" />
          </div>
          <span>页</span>
        </div>
      </div>
    </div>
  </div>
</template>

<style scoped>
.jump_to :deep(.el-input__wrapper) {
  border-radius: 4px;
}
</style>
